use ::ffmpeg::Rational;
use std::{
    path::PathBuf,
    sync::{Arc, mpsc},
};
use tokio::sync::oneshot;

#[cfg(target_os = "macos")]
mod avassetreader;
mod ffmpeg;
mod frame_converter;

pub struct DecodedFrame {
    data: Arc<Vec<u8>>,
    width: u32,
    height: u32,
}

impl DecodedFrame {
    pub fn new(data: Vec<u8>, width: u32, height: u32) -> Self {
        Self {
            data: Arc::new(data),
            width,
            height,
        }
    }

    pub fn data(&self) -> &[u8] {
        &self.data
    }

    pub fn width(&self) -> u32 {
        self.width
    }

    pub fn height(&self) -> u32 {
        self.height
    }
}

pub enum VideoDecoderMessage {
    GetFrame(f32, tokio::sync::oneshot::Sender<DecodedFrame>),
}

pub fn pts_to_frame(pts: i64, time_base: Rational, fps: u32) -> u32 {
    (fps as f64 * ((pts as f64 * time_base.numerator() as f64) / (time_base.denominator() as f64)))
        .round() as u32
}

pub const FRAME_CACHE_SIZE: usize = 500;

#[derive(Clone)]
pub struct AsyncVideoDecoderHandle {
    sender: mpsc::Sender<VideoDecoderMessage>,
    offset: f64,
}

impl AsyncVideoDecoderHandle {
    pub async fn get_frame(&self, time: f32) -> Option<DecodedFrame> {
        let (tx, rx) = tokio::sync::oneshot::channel();
        self.sender
            .send(VideoDecoderMessage::GetFrame(self.get_time(time), tx))
            .unwrap();
        rx.await.ok()
    }

    pub fn get_time(&self, time: f32) -> f32 {
        time + self.offset as f32
    }
}

pub async fn spawn_decoder(
    name: &'static str,
    path: PathBuf,
    fps: u32,
    offset: f64,
) -> Result<AsyncVideoDecoderHandle, String> {
    let (ready_tx, ready_rx) = oneshot::channel::<Result<(), String>>();
    let (tx, rx) = mpsc::channel();

    let handle = AsyncVideoDecoderHandle { sender: tx, offset };

    let path_display = path.display().to_string();

    if cfg!(target_os = "macos") {
        #[cfg(target_os = "macos")]
        avassetreader::AVAssetReaderDecoder::spawn(name, path, fps, rx, ready_tx);
    } else {
        ffmpeg::FfmpegDecoder::spawn(name, path, fps, rx, ready_tx)
            .map_err(|e| format!("'{name}' decoder / {e}"))?;
    }

    match tokio::time::timeout(std::time::Duration::from_secs(30), ready_rx).await {
        Ok(result) => result
            .map_err(|e| format!("'{name}' decoder channel closed: {e}"))?
            .map(|()| handle),
        Err(_) => Err(format!(
            "'{name}' decoder timed out after 30s initializing: {path_display}"
        )),
    }
}
